package com.webbdong.aop.agent.test.bytebuddy;

import com.webbdong.aop.agent.SpringAopAgent;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.ParameterList;
import net.bytebuddy.description.modifier.Ownership;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

import static net.bytebuddy.matcher.ElementMatchers.*;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;

/**
 * @program: spring-agent
 * @description:
 * @author: zt22328 赵儒定
 * @create: 2023-03-31 09:15
 **/
public class BytebuddyTemplate {

    private List<BytebuddyVisitDefine> visits;

    public BytebuddyTemplate(List<BytebuddyVisitDefine> visits) {
        this.visits = visits;
    }

    public void start() {
        ByteBuddyAgent.install();
        AgentBuilder agentBuilder = new AgentBuilder.Default();
        agentBuilder
                .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
                .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
//                .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
                .ignore(ignore())
                .type(type())
                .transform(transformer())
                .with(new DefaultAgentListener())
                .installOnByteBuddyAgent();
    }

    public AgentBuilder.Transformer transformer() {
        return (builder, typeDescription, classLoader, javaModule)-> {
            String actualName = typeDescription.getActualName();
            Optional<BytebuddyVisitDefine> first = visits.stream()
                    .filter(e -> e.className.equals(actualName)).findFirst();
            if (!first.isPresent()) {
                return builder;
            }

            BytebuddyVisitDefine define = first.get();

            builder = builder.visit(Advice.to(define.getAdviceInterceptor())
                    .on(m->{
                        if (!m.isMethod()) {
                            return false;
                        }
                        for (BytebuddyVisitBase.MethodDefinition method : define.getMethods()) {
                            if (m.getActualName().equals(method.getName())
                                    && (method.isAllMethods() || paramTypesMatch(m.getParameters(), method))) {
                                return true;
                            }
                        }
                        return false;
                    })
            );
            if (isEmpty(define.getDefineFieldName())) {
                return builder;
            }
            return builder.defineField(define.getDefineFieldName(), define.getDefineFieldType(), define.getDefineForFields());
        };

    }

    private boolean isEmpty(String str) {
        return str == null || str.length() <= 0;
    }

    private boolean paramTypesMatch(ParameterList<?> parameters, BytebuddyVisitBase.MethodDefinition method) {
        if (method.isAllMethods()) {
            return true;
        }
        List<String> paramTypes = method.getParamTypes();
        if (isEmpty(parameters) && isEmpty(paramTypes)) {
            return true;
        }
        if (parameters.size() != paramTypes.size()) {
            return false;
        }
        int num = 0;
        for (int i = 0; i < parameters.size(); i++) {
            if (parameters.get(i).getType().getTypeName().equals(paramTypes.get(i))) {
                num ++;
            }
        }
        return parameters.size() == num;
    }

    public static boolean isEmpty(List list) {
        return list == null || list.isEmpty();
    }

    private ElementMatcher<? super TypeDescription> type() {
        List<String> names = classNameMatches();
        if (isEmpty(names)) {
            return ElementMatchers.none();
        }
        return ElementMatchers.namedOneOf(names.toArray(new String[names.size()]));
    }

    /**
     * 排除包
     * @return
     */
    private ElementMatcher<? super TypeDescription> ignore() {
        ElementMatcher.Junction<NamedElement> junction = nameStartsWith("net.bytebuddy.")
                .or(nameStartsWith("org.slf4j."))
                .or(nameStartsWith("org.groovy."))
                .or(nameContains("javassist"))
                .or(nameContains(".asm."))
                .or(nameContains(".reflectasm."))
                .or(nameStartsWith("sun.reflect"));
        List<String> startsWiths = ignoreNameStartsWiths();
        if (startsWiths != null && !startsWiths.isEmpty()) {
            startsWiths.forEach(s->{
                junction.or(nameStartsWith(s));
            });
        }
        List<String> contains = ignoreNameContains();
        if (contains != null && !contains.isEmpty()) {
            contains.forEach(s->{
                junction.or(nameContains(s));
            });
        }
        return junction;
    }

    protected List<String> ignoreNameStartsWiths() {
        return Collections.EMPTY_LIST;
    }

    protected List<String> ignoreNameContains() {
        return Collections.EMPTY_LIST;
    }

    protected void withs(AgentBuilder agentBuilder) {

    }

    private List<String> classNameMatches() {
        if (isEmpty(this.visits)) {
            return Collections.EMPTY_LIST;
        }
        return this.visits.stream().map(BytebuddyVisitBase::getClassName).collect(Collectors.toList());
    }



    public static class DefaultAgentListener extends AgentBuilder.Listener.Adapter {

        @Override
        public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
        }

        public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {
            System.out.println(typeName);
            System.out.println(throwable.getMessage());
        }

    }

}
